Dynamic Pricing for Urban Parking Lots¶
Capstone Project - Summer Analytics 2025
Author: Himanshu Kundan Tapde
Date: July 7th, 2025
Overview:¶
This notebook implements three progressively intelligent pricing models for 14 urban parking lots using real-time streaming data (Pathway), pandas, and numpy. We demonstrate:
- A baseline linear model: A simple model based on occupancy.
- A demand-based model: Incorporating occupancy, queue length, traffic conditions, and special day flags.
- A competitive pricing model: Considering geographic proximity and offering optional rerouting suggestions.
Installation of Required Libraries¶
This notebook requires the following libraries:
- pathway: For building and running data pipelines.
- bokeh: For interactive data visualization.
- pandas: For data manipulation and analysis.
- numpy: For numerical operations.
- geopy: For calculating distances between geographical points.
These libraries are installed using pip in the following cell.
!pip install -U pathway bokeh pandas numpy geopy
Requirement already satisfied: pathway in /usr/local/lib/python3.11/dist-packages (0.24.1) Requirement already satisfied: bokeh in /usr/local/lib/python3.11/dist-packages (3.7.3) Requirement already satisfied: pandas in /usr/local/lib/python3.11/dist-packages (2.3.0) Requirement already satisfied: numpy in /usr/local/lib/python3.11/dist-packages (2.3.1) Requirement already satisfied: geopy in /usr/local/lib/python3.11/dist-packages (2.4.1) Requirement already satisfied: aiohttp>=3.8.4 in /usr/local/lib/python3.11/dist-packages (from pathway) (3.11.15) Requirement already satisfied: click>=8.1 in /usr/local/lib/python3.11/dist-packages (from pathway) (8.2.1) Requirement already satisfied: h3>=4 in /usr/local/lib/python3.11/dist-packages (from pathway) (4.3.0) Requirement already satisfied: scikit-learn>=1.0 in /usr/local/lib/python3.11/dist-packages (from pathway) (1.6.1) Requirement already satisfied: shapely>=2.0.1 in /usr/local/lib/python3.11/dist-packages (from pathway) (2.1.1) Requirement already satisfied: pyarrow<19.0.0,>=10.0.0 in /usr/local/lib/python3.11/dist-packages (from pathway) (18.1.0) Requirement already satisfied: requests>=2.31.0 in /usr/local/lib/python3.11/dist-packages (from pathway) (2.32.3) Requirement already satisfied: python-sat>=0.1.8.dev0 in /usr/local/lib/python3.11/dist-packages (from pathway) (1.8.dev17) Requirement already satisfied: beartype<0.16.0,>=0.14.0 in /usr/local/lib/python3.11/dist-packages (from pathway) (0.15.0) Requirement already satisfied: rich>=12.6.0 in /usr/local/lib/python3.11/dist-packages (from pathway) (13.9.4) Requirement already satisfied: diskcache>=5.2.1 in /usr/local/lib/python3.11/dist-packages (from pathway) (5.6.3) Requirement already satisfied: boto3<1.36.0,>=1.26.76 in /usr/local/lib/python3.11/dist-packages (from pathway) (1.35.93) Requirement already satisfied: aiobotocore==2.17.0 in /usr/local/lib/python3.11/dist-packages (from pathway) (2.17.0) Requirement already satisfied: google-api-python-client>=2.108.0 in /usr/local/lib/python3.11/dist-packages (from pathway) (2.174.0) Requirement already satisfied: typing-extensions>=4.8.0 in /usr/local/lib/python3.11/dist-packages (from pathway) (4.14.0) Requirement already satisfied: panel>=1.3.1 in /usr/local/lib/python3.11/dist-packages (from pathway) (1.7.2) Requirement already satisfied: jupyter-bokeh>=3.0.7 in /usr/local/lib/python3.11/dist-packages (from pathway) (4.0.5) Requirement already satisfied: jmespath>=1.0.1 in /usr/local/lib/python3.11/dist-packages (from pathway) (1.0.1) Requirement already satisfied: aiohttp-cors>=0.7.0 in /usr/local/lib/python3.11/dist-packages (from pathway) (0.8.1) Requirement already satisfied: opentelemetry-api>=1.22.0 in /usr/local/lib/python3.11/dist-packages (from pathway) (1.34.1) Requirement already satisfied: opentelemetry-sdk>=1.22.0 in /usr/local/lib/python3.11/dist-packages (from pathway) (1.34.1) Requirement already satisfied: opentelemetry-exporter-otlp-proto-grpc>=1.22.0 in /usr/local/lib/python3.11/dist-packages (from pathway) (1.34.1) Requirement already satisfied: fs>=2.4.16 in /usr/local/lib/python3.11/dist-packages (from pathway) (2.4.16) Requirement already satisfied: async-lru>=2.0.4 in /usr/local/lib/python3.11/dist-packages (from pathway) (2.0.5) Requirement already satisfied: networkx>=3.2.1 in /usr/local/lib/python3.11/dist-packages (from pathway) (3.5) Requirement already satisfied: google-cloud-pubsub>=2.21.1 in /usr/local/lib/python3.11/dist-packages (from pathway) (2.30.0) Requirement already satisfied: google-cloud-bigquery~=3.29.0 in /usr/local/lib/python3.11/dist-packages (from pathway) (3.29.0) Requirement already satisfied: pydantic~=2.9.0 in /usr/local/lib/python3.11/dist-packages (from pathway) (2.9.2) Requirement already satisfied: gitpython>=3.1.43 in /usr/local/lib/python3.11/dist-packages (from pathway) (3.1.44) Requirement already satisfied: deltalake<0.18.0,>=0.17.0 in /usr/local/lib/python3.11/dist-packages (from pathway) (0.17.4) Requirement already satisfied: aioitertools<1.0.0,>=0.5.1 in /usr/local/lib/python3.11/dist-packages (from aiobotocore==2.17.0->pathway) (0.12.0) Requirement already satisfied: botocore<1.35.94,>=1.35.74 in /usr/local/lib/python3.11/dist-packages (from aiobotocore==2.17.0->pathway) (1.35.93) Requirement already satisfied: python-dateutil<3.0.0,>=2.1 in /usr/local/lib/python3.11/dist-packages (from aiobotocore==2.17.0->pathway) (2.9.0.post0) Requirement already satisfied: multidict<7.0.0,>=6.0.0 in /usr/local/lib/python3.11/dist-packages (from aiobotocore==2.17.0->pathway) (6.6.3) Requirement already satisfied: urllib3!=2.2.0,<3,>=1.25.4 in /usr/local/lib/python3.11/dist-packages (from aiobotocore==2.17.0->pathway) (2.4.0) Requirement already satisfied: wrapt<2.0.0,>=1.10.10 in /usr/local/lib/python3.11/dist-packages (from aiobotocore==2.17.0->pathway) (1.17.2) Requirement already satisfied: Jinja2>=2.9 in /usr/local/lib/python3.11/dist-packages (from bokeh) (3.1.6) Requirement already satisfied: contourpy>=1.2 in /usr/local/lib/python3.11/dist-packages (from bokeh) (1.3.2) Requirement already satisfied: narwhals>=1.13 in /usr/local/lib/python3.11/dist-packages (from bokeh) (1.45.0) Requirement already satisfied: packaging>=16.8 in /usr/local/lib/python3.11/dist-packages (from bokeh) (24.2) Requirement already satisfied: pillow>=7.1.0 in /usr/local/lib/python3.11/dist-packages (from bokeh) (11.2.1) Requirement already satisfied: PyYAML>=3.10 in /usr/local/lib/python3.11/dist-packages (from bokeh) (6.0.2) Requirement already satisfied: tornado>=6.2 in /usr/local/lib/python3.11/dist-packages (from bokeh) (6.4.2) Requirement already satisfied: xyzservices>=2021.09.1 in /usr/local/lib/python3.11/dist-packages (from bokeh) (2025.4.0) Requirement already satisfied: pytz>=2020.1 in /usr/local/lib/python3.11/dist-packages (from pandas) (2025.2) Requirement already satisfied: tzdata>=2022.7 in /usr/local/lib/python3.11/dist-packages (from pandas) (2025.2) Requirement already satisfied: geographiclib<3,>=1.52 in /usr/local/lib/python3.11/dist-packages (from geopy) (2.0) Requirement already satisfied: aiohappyeyeballs>=2.3.0 in /usr/local/lib/python3.11/dist-packages (from aiohttp>=3.8.4->pathway) (2.6.1) Requirement already satisfied: aiosignal>=1.1.2 in /usr/local/lib/python3.11/dist-packages (from aiohttp>=3.8.4->pathway) (1.3.2) Requirement already satisfied: attrs>=17.3.0 in /usr/local/lib/python3.11/dist-packages (from aiohttp>=3.8.4->pathway) (25.3.0) Requirement already satisfied: frozenlist>=1.1.1 in /usr/local/lib/python3.11/dist-packages (from aiohttp>=3.8.4->pathway) (1.7.0) Requirement already satisfied: propcache>=0.2.0 in /usr/local/lib/python3.11/dist-packages (from aiohttp>=3.8.4->pathway) (0.3.2) Requirement already satisfied: yarl<2.0,>=1.17.0 in /usr/local/lib/python3.11/dist-packages (from aiohttp>=3.8.4->pathway) (1.20.1) Requirement already satisfied: s3transfer<0.11.0,>=0.10.0 in /usr/local/lib/python3.11/dist-packages (from boto3<1.36.0,>=1.26.76->pathway) (0.10.4) Requirement already satisfied: pyarrow-hotfix in /usr/local/lib/python3.11/dist-packages (from deltalake<0.18.0,>=0.17.0->pathway) (0.7) Requirement already satisfied: appdirs~=1.4.3 in /usr/local/lib/python3.11/dist-packages (from fs>=2.4.16->pathway) (1.4.4) Requirement already satisfied: setuptools in /usr/local/lib/python3.11/dist-packages (from fs>=2.4.16->pathway) (75.2.0) Requirement already satisfied: six~=1.10 in /usr/local/lib/python3.11/dist-packages (from fs>=2.4.16->pathway) (1.17.0) Requirement already satisfied: gitdb<5,>=4.0.1 in /usr/local/lib/python3.11/dist-packages (from gitpython>=3.1.43->pathway) (4.0.12) Requirement already satisfied: httplib2<1.0.0,>=0.19.0 in /usr/local/lib/python3.11/dist-packages (from google-api-python-client>=2.108.0->pathway) (0.22.0) Requirement already satisfied: google-auth!=2.24.0,!=2.25.0,<3.0.0,>=1.32.0 in /usr/local/lib/python3.11/dist-packages (from google-api-python-client>=2.108.0->pathway) (2.38.0) Requirement already satisfied: google-auth-httplib2<1.0.0,>=0.2.0 in /usr/local/lib/python3.11/dist-packages (from google-api-python-client>=2.108.0->pathway) (0.2.0) Requirement already satisfied: google-api-core!=2.0.*,!=2.1.*,!=2.2.*,!=2.3.0,<3.0.0,>=1.31.5 in /usr/local/lib/python3.11/dist-packages (from google-api-python-client>=2.108.0->pathway) (2.25.1) Requirement already satisfied: uritemplate<5,>=3.0.1 in /usr/local/lib/python3.11/dist-packages (from google-api-python-client>=2.108.0->pathway) (4.2.0) Requirement already satisfied: google-cloud-core<3.0.0dev,>=2.4.1 in /usr/local/lib/python3.11/dist-packages (from google-cloud-bigquery~=3.29.0->pathway) (2.4.3) Requirement already satisfied: google-resumable-media<3.0dev,>=2.0.0 in /usr/local/lib/python3.11/dist-packages (from google-cloud-bigquery~=3.29.0->pathway) (2.7.2) Requirement already satisfied: grpcio<2.0.0,>=1.51.3 in /usr/local/lib/python3.11/dist-packages (from google-cloud-pubsub>=2.21.1->pathway) (1.73.1) Requirement already satisfied: proto-plus<2.0.0,>=1.22.0 in /usr/local/lib/python3.11/dist-packages (from google-cloud-pubsub>=2.21.1->pathway) (1.26.1) Requirement already satisfied: protobuf!=4.21.0,!=4.21.1,!=4.21.2,!=4.21.3,!=4.21.4,!=4.21.5,<7.0.0,>=3.20.2 in /usr/local/lib/python3.11/dist-packages (from google-cloud-pubsub>=2.21.1->pathway) (5.29.5) Requirement already satisfied: grpc-google-iam-v1<1.0.0,>=0.12.4 in /usr/local/lib/python3.11/dist-packages (from google-cloud-pubsub>=2.21.1->pathway) (0.14.2) Requirement already satisfied: grpcio-status>=1.33.2 in /usr/local/lib/python3.11/dist-packages (from google-cloud-pubsub>=2.21.1->pathway) (1.71.2) Requirement already satisfied: MarkupSafe>=2.0 in /usr/local/lib/python3.11/dist-packages (from Jinja2>=2.9->bokeh) (3.0.2) Requirement already satisfied: ipywidgets==8.* in /usr/local/lib/python3.11/dist-packages (from jupyter-bokeh>=3.0.7->pathway) (8.1.7) Requirement already satisfied: comm>=0.1.3 in /usr/local/lib/python3.11/dist-packages (from ipywidgets==8.*->jupyter-bokeh>=3.0.7->pathway) (0.2.2) Requirement already satisfied: ipython>=6.1.0 in /usr/local/lib/python3.11/dist-packages (from ipywidgets==8.*->jupyter-bokeh>=3.0.7->pathway) (7.34.0) Requirement already satisfied: traitlets>=4.3.1 in /usr/local/lib/python3.11/dist-packages (from ipywidgets==8.*->jupyter-bokeh>=3.0.7->pathway) (5.7.1) Requirement already satisfied: widgetsnbextension~=4.0.14 in /usr/local/lib/python3.11/dist-packages (from ipywidgets==8.*->jupyter-bokeh>=3.0.7->pathway) (4.0.14) Requirement already satisfied: jupyterlab_widgets~=3.0.15 in /usr/local/lib/python3.11/dist-packages (from ipywidgets==8.*->jupyter-bokeh>=3.0.7->pathway) (3.0.15) Requirement already satisfied: importlib-metadata<8.8.0,>=6.0 in /usr/local/lib/python3.11/dist-packages (from opentelemetry-api>=1.22.0->pathway) (8.7.0) Requirement already satisfied: googleapis-common-protos~=1.52 in /usr/local/lib/python3.11/dist-packages (from opentelemetry-exporter-otlp-proto-grpc>=1.22.0->pathway) (1.70.0) Requirement already satisfied: opentelemetry-exporter-otlp-proto-common==1.34.1 in /usr/local/lib/python3.11/dist-packages (from opentelemetry-exporter-otlp-proto-grpc>=1.22.0->pathway) (1.34.1) Requirement already satisfied: opentelemetry-proto==1.34.1 in /usr/local/lib/python3.11/dist-packages (from opentelemetry-exporter-otlp-proto-grpc>=1.22.0->pathway) (1.34.1) Requirement already satisfied: opentelemetry-semantic-conventions==0.55b1 in /usr/local/lib/python3.11/dist-packages (from opentelemetry-sdk>=1.22.0->pathway) (0.55b1) Requirement already satisfied: bleach in /usr/local/lib/python3.11/dist-packages (from panel>=1.3.1->pathway) (6.2.0) Requirement already satisfied: linkify-it-py in /usr/local/lib/python3.11/dist-packages (from panel>=1.3.1->pathway) (2.0.3) Requirement already satisfied: markdown in /usr/local/lib/python3.11/dist-packages (from panel>=1.3.1->pathway) (3.8.2) Requirement already satisfied: markdown-it-py in /usr/local/lib/python3.11/dist-packages (from panel>=1.3.1->pathway) (3.0.0) Requirement already satisfied: mdit-py-plugins in /usr/local/lib/python3.11/dist-packages (from panel>=1.3.1->pathway) (0.4.2) Requirement already satisfied: param<3.0,>=2.1.0 in /usr/local/lib/python3.11/dist-packages (from panel>=1.3.1->pathway) (2.2.1) Requirement already satisfied: pyviz-comms>=2.0.0 in /usr/local/lib/python3.11/dist-packages (from panel>=1.3.1->pathway) (3.0.6) Requirement already satisfied: tqdm in /usr/local/lib/python3.11/dist-packages (from panel>=1.3.1->pathway) (4.67.1) Requirement already satisfied: annotated-types>=0.6.0 in /usr/local/lib/python3.11/dist-packages (from pydantic~=2.9.0->pathway) (0.7.0) Requirement already satisfied: pydantic-core==2.23.4 in /usr/local/lib/python3.11/dist-packages (from pydantic~=2.9.0->pathway) (2.23.4) Requirement already satisfied: charset-normalizer<4,>=2 in /usr/local/lib/python3.11/dist-packages (from requests>=2.31.0->pathway) (3.4.2) Requirement already satisfied: idna<4,>=2.5 in /usr/local/lib/python3.11/dist-packages (from requests>=2.31.0->pathway) (3.10) Requirement already satisfied: certifi>=2017.4.17 in /usr/local/lib/python3.11/dist-packages (from requests>=2.31.0->pathway) (2025.6.15) Requirement already satisfied: pygments<3.0.0,>=2.13.0 in /usr/local/lib/python3.11/dist-packages (from rich>=12.6.0->pathway) (2.19.2) Requirement already satisfied: scipy>=1.6.0 in /usr/local/lib/python3.11/dist-packages (from scikit-learn>=1.0->pathway) (1.15.3) Requirement already satisfied: joblib>=1.2.0 in /usr/local/lib/python3.11/dist-packages (from scikit-learn>=1.0->pathway) (1.5.1) Requirement already satisfied: threadpoolctl>=3.1.0 in /usr/local/lib/python3.11/dist-packages (from scikit-learn>=1.0->pathway) (3.6.0) Requirement already satisfied: smmap<6,>=3.0.1 in /usr/local/lib/python3.11/dist-packages (from gitdb<5,>=4.0.1->gitpython>=3.1.43->pathway) (5.0.2) Requirement already satisfied: cachetools<6.0,>=2.0.0 in /usr/local/lib/python3.11/dist-packages (from google-auth!=2.24.0,!=2.25.0,<3.0.0,>=1.32.0->google-api-python-client>=2.108.0->pathway) (5.5.2) Requirement already satisfied: pyasn1-modules>=0.2.1 in /usr/local/lib/python3.11/dist-packages (from google-auth!=2.24.0,!=2.25.0,<3.0.0,>=1.32.0->google-api-python-client>=2.108.0->pathway) (0.4.2) Requirement already satisfied: rsa<5,>=3.1.4 in /usr/local/lib/python3.11/dist-packages (from google-auth!=2.24.0,!=2.25.0,<3.0.0,>=1.32.0->google-api-python-client>=2.108.0->pathway) (4.9.1) Requirement already satisfied: google-crc32c<2.0dev,>=1.0 in /usr/local/lib/python3.11/dist-packages (from google-resumable-media<3.0dev,>=2.0.0->google-cloud-bigquery~=3.29.0->pathway) (1.7.1) Requirement already satisfied: pyparsing!=3.0.0,!=3.0.1,!=3.0.2,!=3.0.3,<4,>=2.4.2 in /usr/local/lib/python3.11/dist-packages (from httplib2<1.0.0,>=0.19.0->google-api-python-client>=2.108.0->pathway) (3.2.3) Requirement already satisfied: zipp>=3.20 in /usr/local/lib/python3.11/dist-packages (from importlib-metadata<8.8.0,>=6.0->opentelemetry-api>=1.22.0->pathway) (3.23.0) Requirement already satisfied: mdurl~=0.1 in /usr/local/lib/python3.11/dist-packages (from markdown-it-py->panel>=1.3.1->pathway) (0.1.2) Requirement already satisfied: webencodings in /usr/local/lib/python3.11/dist-packages (from bleach->panel>=1.3.1->pathway) (0.5.1) Requirement already satisfied: uc-micro-py in /usr/local/lib/python3.11/dist-packages (from linkify-it-py->panel>=1.3.1->pathway) (1.0.3) Requirement already satisfied: jedi>=0.16 in /usr/local/lib/python3.11/dist-packages (from ipython>=6.1.0->ipywidgets==8.*->jupyter-bokeh>=3.0.7->pathway) (0.19.2) Requirement already satisfied: decorator in /usr/local/lib/python3.11/dist-packages (from ipython>=6.1.0->ipywidgets==8.*->jupyter-bokeh>=3.0.7->pathway) (4.4.2) Requirement already satisfied: pickleshare in /usr/local/lib/python3.11/dist-packages (from ipython>=6.1.0->ipywidgets==8.*->jupyter-bokeh>=3.0.7->pathway) (0.7.5) Requirement already satisfied: prompt-toolkit!=3.0.0,!=3.0.1,<3.1.0,>=2.0.0 in /usr/local/lib/python3.11/dist-packages (from ipython>=6.1.0->ipywidgets==8.*->jupyter-bokeh>=3.0.7->pathway) (3.0.51) Requirement already satisfied: backcall in /usr/local/lib/python3.11/dist-packages (from ipython>=6.1.0->ipywidgets==8.*->jupyter-bokeh>=3.0.7->pathway) (0.2.0) Requirement already satisfied: matplotlib-inline in /usr/local/lib/python3.11/dist-packages (from ipython>=6.1.0->ipywidgets==8.*->jupyter-bokeh>=3.0.7->pathway) (0.1.7) Requirement already satisfied: pexpect>4.3 in /usr/local/lib/python3.11/dist-packages (from ipython>=6.1.0->ipywidgets==8.*->jupyter-bokeh>=3.0.7->pathway) (4.9.0) Requirement already satisfied: pyasn1<0.7.0,>=0.6.1 in /usr/local/lib/python3.11/dist-packages (from pyasn1-modules>=0.2.1->google-auth!=2.24.0,!=2.25.0,<3.0.0,>=1.32.0->google-api-python-client>=2.108.0->pathway) (0.6.1) Requirement already satisfied: parso<0.9.0,>=0.8.4 in /usr/local/lib/python3.11/dist-packages (from jedi>=0.16->ipython>=6.1.0->ipywidgets==8.*->jupyter-bokeh>=3.0.7->pathway) (0.8.4) Requirement already satisfied: ptyprocess>=0.5 in /usr/local/lib/python3.11/dist-packages (from pexpect>4.3->ipython>=6.1.0->ipywidgets==8.*->jupyter-bokeh>=3.0.7->pathway) (0.7.0) Requirement already satisfied: wcwidth in /usr/local/lib/python3.11/dist-packages (from prompt-toolkit!=3.0.0,!=3.0.1,<3.1.0,>=2.0.0->ipython>=6.1.0->ipywidgets==8.*->jupyter-bokeh>=3.0.7->pathway) (0.2.13)
# Core libraries and Pathway
import time
import pandas as pd
import numpy as np
import pathway as pw
from pathway.internals.dtype import DATE_TIME_UTC as DateTimeUtc
# For competitor distance calc
from geopy.distance import geodesic
# For plotting
from bokeh.plotting import figure, show, output_notebook
from bokeh.models import ColumnDataSource, Select, CustomJS
from bokeh.layouts import column
# Enable Bokeh in notebook
output_notebook()
This cell reads the parking lot data from a CSV file named "dataset.csv". It then performs the following data processing steps:
- Reads the CSV: Loads the data into a pandas DataFrame.
- Combines and Converts Datetime: Combines the
LastUpdatedDateandLastUpdatedTimecolumns into a singletimestampcolumn, converts it to datetime objects, localizes the timezone to 'Asia/Kolkata', and then converts it to UTC. - Drops and Renames Columns: Removes the original date, time, and ID columns and renames the remaining columns to match a predefined schema (
lot_id,capacity,lat,lon,occupancy,vehicle,traffic,queue,special).
# 1. Read CSV
df = pd.read_csv("dataset.csv")
# 2. Combine date/time and convert ISTāUTC
df["timestamp"] = (
pd.to_datetime(df["LastUpdatedDate"] + " " + df["LastUpdatedTime"],
format="%d-%m-%Y %H:%M:%S")
.dt.tz_localize("Asia/Kolkata")
.dt.tz_convert("UTC")
)
# 3. Drop originals and rename to match schema
df = (
df
.drop(columns=["LastUpdatedDate", "LastUpdatedTime", "ID"])
.rename(columns={
"SystemCodeNumber": "lot_id",
"Capacity": "capacity",
"Latitude": "lat",
"Longitude": "lon",
"Occupancy": "occupancy",
"VehicleType": "vehicle",
"TrafficConditionNearby": "traffic",
"QueueLength": "queue",
"IsSpecialDay": "special"
})
)
This cell sets up the data streaming using Pathway.
- Defines the Schema: Creates a
Pathway.Schemaclass to define the structure and data types of the incoming data stream. - Creates a Stream Connector: Defines a
StreamSubjectclass that simulates a real-time data stream by iterating through the rows of the pre-loaded pandas DataFrame (df) and emitting them as Pathway events. A small time delay is added to simulate streaming. - Hooks up the Stream: Uses
pw.io.python.readto connect theStreamSubjectto a Pathway stream, making the data available for real-time processing.autocommit_duration_msis set to control how often data changes are committed.
# Single Schema definition
class Schema(pw.Schema):
timestamp: DateTimeUtc
lot_id: str
capacity: int
lat: float
lon: float
occupancy: int
queue: int
special: int
vehicle: str
traffic: str
# Simulated stream connector
class StreamSubject(pw.io.python.ConnectorSubject):
def __init__(self, df):
super().__init__()
self.df = df.sort_values("timestamp")
def run(self):
for _, row in self.df.iterrows():
self.next(**row.to_dict())
time.sleep(0.01) # 10 ms per event for faster debug
# Hook up the stream
stream = pw.io.python.read(
StreamSubject(df),
schema=Schema,
autocommit_duration_ms=50
)
## 1. Model 1: Baseline Linear Pricing
We start with a simple linear update rule:
$$Price_{t+1} = Price_t + \alpha \times \frac{Occupancy}{Capacity}$$
where \(\alpha\) is a tuning parameter controlling sensitivity.
This serves as our reference point for more advanced models.
This cell extracts the unique latitude and longitude for each parking lot from the DataFrame. This information is stored in a dictionary called lot_locations, which is used later in the competitive pricing model to find nearby lots.
# Base price
BASE_PRICE = 10
# Model 1: Simple linear
def price_linear(occupancy, capacity, base=BASE_PRICE, alpha=10):
return base + alpha * (occupancy / capacity)
# Model 2: Demand-based
VEHICLE_WEIGHTS = {'car': 0.05, 'bike': 0.02, 'truck': 0.1}
TRAFFIC_WEIGHTS = {'low': 0.0, 'medium': 0.05, 'high': 0.1}
def price_demand(occupancy, capacity, queue, traffic, special, vehicle, base=BASE_PRICE):
occ_rate = occupancy / capacity
v_w = VEHICLE_WEIGHTS.get(vehicle, 0)
t_w = TRAFFIC_WEIGHTS.get(traffic, 0)
demand = 0.5*occ_rate + 0.3*queue - 0.2*t_w + 0.1*special + v_w
demand = np.clip(demand, 0, 1)
price = base * (1 + demand)
return float(np.clip(price, base*0.5, base*2))
# Model 3: Competitive with global latest_prices dict
latest_prices = {}
def price_competitive(lot_id, occupancy, capacity, queue, traffic, special, vehicle, lat, lon):
# Get demand price
d_price = price_demand(occupancy, capacity, queue, traffic, special, vehicle)
# Find nearby lots (within 1 km)
nearby = []
for other, loc in lot_locations.items():
if other != lot_id and geodesic((lat,lon),(loc['lat'],loc['lon'])).km<=1:
nearby.append(other)
comps = [latest_prices.get(l) for l in nearby if l in latest_prices]
price = d_price
if occupancy >= capacity and comps:
if min(comps) < d_price:
price = d_price * 0.95 # discount
elif max(comps) > d_price:
price = d_price * 1.05 # premium
# Save for next round
latest_prices[lot_id] = float(np.clip(price, BASE_PRICE*0.5, BASE_PRICE*2))
return latest_prices[lot_id]
# Extract unique lot lat/lon for competitor lookup
lot_locations = (
df[['lot_id','lat','lon']]
.drop_duplicates('lot_id')
.set_index('lot_id')
.to_dict('index')
)
This cell uses Pathway to apply the three pricing models to the streaming data.
- Select and Compute Prices: It selects the necessary columns from the data stream and uses
pw.applyto computeprice1,price2, andprice3for each data point using the previously defined functions. - Write to CSV: The computed prices and relevant data are written to a CSV file named
/tmp/prices.csvusingpw.io.csv.write. - Execute Pipeline:
pw.run()starts the Pathway pipeline, processing the data stream and generating the output CSV file.
# Select and compute all three prices
price_table = stream.select(
ts = stream.timestamp,
lot = stream.lot_id,
occ = stream.occupancy,
price1 = pw.apply(price_linear, stream.occupancy, stream.capacity),
price2 = pw.apply(price_demand,
stream.occupancy, stream.capacity,
stream.queue, stream.traffic,
stream.special, stream.vehicle),
price3 = pw.apply(price_competitive,
stream.lot_id, stream.occupancy,
stream.capacity, stream.queue,
stream.traffic, stream.special,
stream.vehicle, stream.lat, stream.lon)
)
# Write results to CSV
pw.io.csv.write(price_table, "/tmp/prices.csv")
# Execute the pipeline
pw.run()
Output()
/usr/local/lib/python3.11/dist-packages/beartype/_util/hint/pep/utilpeptest.py:311: BeartypeDecorHintPep585DeprecationWarning: PEP 484 type hint typing.Iterable[pathway.internals.expression.ColumnReference] deprecated by PEP 585. This hint is scheduled for removal in the first Python version released after October 5th, 2025. To resolve this, import this hint from "beartype.typing" rather than "typing". For further commentary and alternatives, see also:
https://beartype.readthedocs.io/en/latest/api_roar/#pep-585-deprecations
warn(
WARNING:pathway_engine.connectors.monitoring:PythonReader: Closing the data source
# Load the generated prices file
df_prices = pd.read_csv("/tmp/prices.csv")
df_prices['ts'] = pd.to_datetime(df_prices['ts'])
df_prices.head()
| ts | lot | occ | price1 | price2 | price3 | time | diff | |
|---|---|---|---|---|---|---|---|---|
| 0 | 2016-10-04 02:29:00+00:00 | BHMNCPHST01 | 237 | 11.975000 | 17.187500 | 17.187500 | 1751883605432 | 1 |
| 1 | 2016-10-04 02:29:00+00:00 | BHMMBMMBX01 | 264 | 13.842795 | 18.421397 | 18.421397 | 1751883605432 | 1 |
| 2 | 2016-10-04 02:29:00+00:00 | BHMBCCMKT01 | 61 | 11.057192 | 14.028596 | 14.028596 | 1751883605432 | 1 |
| 3 | 2016-10-04 02:29:00+00:00 | BHMNCPNST01 | 249 | 15.134021 | 19.067010 | 19.067010 | 1751883605446 | 1 |
| 4 | 2016-10-04 02:29:00+00:00 | BHMEURBRD01 | 117 | 12.489362 | 17.744681 | 17.744681 | 1751883605446 | 1 |
This cell generates an interactive Bokeh plot to visualize the dynamic prices calculated by the three models for each parking lot.
- Prepare Data: Ensures the timestamp column is in datetime format and prepares two
ColumnDataSourceobjects: one holding all data (full_src) and another for the currently selected lot (src). - Initialize Plot: Sets up the Bokeh plot with a datetime x-axis and defines the initial title.
- Add Lines: Adds lines to the plot for
price1,price2, andprice3using thesrcdata source. - Build Selector: Creates a dropdown menu (
Select) populated with the unique parking lot IDs. - Add JavaScript Callback: Implements a
CustomJScallback function that updates thesrcdata source based on the selected lot in the dropdown, triggering the plot to display data for that specific lot. - Show Plot: Displays the dropdown selector and the interactive plot using
bokeh.layouts.columnandbokeh.plotting.show.
from bokeh.plotting import figure, show, output_notebook
from bokeh.models import ColumnDataSource, Select, CustomJS
from bokeh.layouts import column
import pandas as pd
# 1) Make sure ts is datetime
df_prices['ts'] = pd.to_datetime(df_prices['ts'])
# 2) Tell Bokeh to render inline
output_notebook()
# 3) Create two ColumnDataSources:
# - full_src holds all the data permanently
# - src holds only the data for the currently-selected lot
full_src = ColumnDataSource(df_prices)
initial_lot = df_prices['lot'].iloc[0]
mask = df_prices['lot'] == initial_lot
src = ColumnDataSource(df_prices[mask])
# 4) Build the figure using āsrcā
p = figure(x_axis_type="datetime", height=350, width=750,
title="Dynamic Prices per Lot")
p.line('ts', 'price1', source=src, color="blue", legend_label="Linear")
p.line('ts', 'price2', source=src, color="green", legend_label="Demand")
p.line('ts', 'price3', source=src, color="red", legend_label="Competitive")
p.legend.location = "top_left"
# 5) Build the lot selector
lots = sorted(df_prices['lot'].unique())
selector = Select(title="Choose Lot", value=initial_lot, options=lots)
# 6) JS callback: filter full_src ā src
callback = CustomJS(args=dict(full=full_src, view=src, sel=selector), code="""
const all = full.data;
const lot = sel.value;
const ts = all['ts'], p1 = all['price1'], p2 = all['price2'], p3 = all['price3'], lots = all['lot'];
const new_ts = [], new_p1 = [], new_p2 = [], new_p3 = [], new_lot = [];
for(let i=0; i<ts.length; i++){
if(lots[i] === lot){
new_ts.push(ts[i]);
new_p1.push(p1[i]);
new_p2.push(p2[i]);
new_p3.push(p3[i]);
new_lot.push(lots[i]);
}
}
view.data = {
ts: new_ts,
price1: new_p1,
price2: new_p2,
price3: new_p3,
lot: new_lot
};
view.change.emit();
""")
selector.js_on_change('value', callback)
# 7) Show widget + plot
show(column(selector, p))
import pandas as pd
# reload the CSV so we have price1, price2, price3
df_prices = pd.read_csv("/tmp/prices.csv")
df_prices['ts'] = pd.to_datetime(df_prices['ts'])
# Compute a simple competitorāavg price3:
# for each timestamp, average of all OTHER lotsā price3
df_prices['comp_avg_p3'] = df_prices.groupby('ts')['price3']\
.transform(lambda s: (s.sum() - s) / (s.count() - 1))
# Quick sanity check
df_prices.head()
| ts | lot | occ | price1 | price2 | price3 | time | diff | comp_avg_p3 | |
|---|---|---|---|---|---|---|---|---|---|
| 0 | 2016-10-04 02:29:00+00:00 | BHMNCPHST01 | 237 | 11.975000 | 17.187500 | 17.187500 | 1751883605432 | 1 | 17.483380 |
| 1 | 2016-10-04 02:29:00+00:00 | BHMMBMMBX01 | 264 | 13.842795 | 18.421397 | 18.421397 | 1751883605432 | 1 | 17.388465 |
| 2 | 2016-10-04 02:29:00+00:00 | BHMBCCMKT01 | 61 | 11.057192 | 14.028596 | 14.028596 | 1751883605432 | 1 | 17.726373 |
| 3 | 2016-10-04 02:29:00+00:00 | BHMNCPNST01 | 249 | 15.134021 | 19.067010 | 19.067010 | 1751883605446 | 1 | 17.338803 |
| 4 | 2016-10-04 02:29:00+00:00 | BHMEURBRD01 | 117 | 12.489362 | 17.744681 | 17.744681 | 1751883605446 | 1 | 17.440520 |
This cell generates two interactive Bokeh plots that are linked by a dropdown selector, allowing you to visualize different aspects of the pricing models for each parking lot.
- Plot 1: Compares the prices from the Baseline Linear Model (
price1) and the Demand-Based Model (price2). - Plot 2: Compares the prices from the Demand-Based Model (
price2), the Competitive Pricing Model (price3), and the calculated Competitor Average Price (comp_avg_p3).
A dropdown menu is provided to select a specific parking lot, and a JavaScript callback updates both plots simultaneously to display the data for the chosen lot.
from bokeh.plotting import figure, show, output_notebook
from bokeh.models import ColumnDataSource, Select, CustomJS
from bokeh.layouts import column
output_notebook()
# Prepare full and initial filtered sources
full_src = ColumnDataSource(df_prices)
lots = sorted(df_prices['lot'].unique())
initial = lots[0]
mask_init = df_prices['lot'] == initial
src1 = ColumnDataSource(df_prices[mask_init]) # for Plot 1 (price1 vs price2)
src2 = ColumnDataSource(df_prices[mask_init]) # for Plot 2 (price2, price3, comp_avg_p3)
# Plot 1: Model 1 vs Model 2
p1 = figure(x_axis_type="datetime", height=300, width=700,
title="Lot: āModel 1 vs Model 2ā")
p1.line('ts', 'price1', source=src1, color="blue", legend_label="Price1")
p1.line('ts', 'price2', source=src1, color="green", legend_label="Price2")
p1.legend.location = "top_left"
# Plot 2: Demand vs Competitive vs Competitor Avg
p2 = figure(x_axis_type="datetime", height=300, width=700,
title="Lot: āDemand vs Competitive vs Competitor Avgā")
p2.line('ts', 'price2', source=src2, color="green", legend_label="Price2")
p2.line('ts', 'price3', source=src2, color="red", legend_label="Price3_competitive")
p2.line('ts', 'comp_avg_p3', source=src2, color="orange",legend_label="Competitor Avg Price3")
p2.legend.location = "top_left"
# Dropdown selector
selector = Select(title="Select Lot", value=initial, options=lots)
# JS callback to filter full_src ā src1 & src2
callback = CustomJS(args=dict(full=full_src, s1=src1, s2=src2, sel=selector), code="""
const d = full.data;
const lot = sel.value;
const ts = d['ts'], lots = d['lot'];
const p1 = d['price1'], p2 = d['price2'], p3 = d['price3'], ca = d['comp_avg_p3'];
const new1 = {ts:[], price1:[], price2:[]};
const new2 = {ts:[], price2:[], price3:[], comp_avg_p3:[]};
for (let i = 0; i < ts.length; i++) {
if (lots[i] === lot) {
new1.ts.push(ts[i]);
new1.price1.push(p1[i]);
new1.price2.push(p2[i]);
new2.ts.push(ts[i]);
new2.price2.push(p2[i]);
new2.price3.push(p3[i]);
new2.comp_avg_p3.push(ca[i]);
}
}
s1.data = new1; s1.change.emit();
s2.data = new2; s2.change.emit();
""")
selector.js_on_change('value', callback)
# Display both
show(column(selector, p1, p2))
This cell performs exploratory data analysis (EDA) on the dataset and generates several plots to visualize key characteristics:
- Missing values: Prints the percentage of missing values for each column.
- Basic stats: Displays descriptive statistics for the numeric columns (
capacity,occupancy, andqueue). - Occupancy rate distribution: Plots a histogram to show the distribution of the occupancy rate (occupancy divided by capacity).
- Time-of-day pattern: Calculates and plots the average occupancy rate throughout the day to identify any temporal patterns.
- Special day vs. normal day: Generates a box plot to compare the distribution of occupancy rates on special days (flagged as 1) versus normal days (flagged as 0).
import matplotlib.pyplot as plt
# 1. Missing values
print("Missing rates (%):\n", df.isnull().mean()*100)
# 2. Basic stats
print("\nNumeric summary:")
print(df[['capacity','occupancy','queue']].describe())
# 3. Occupancy rate distribution
df['occ_rate'] = df['occupancy'] / df['capacity']
df['occ_rate'].hist(bins=30, figsize=(6,4))
plt.title("Occupancy Rate Distribution")
plt.xlabel("occupancy / capacity")
plt.ylabel("count")
plt.show()
# 4. Time-of-day pattern
df['time'] = df['timestamp'].dt.hour + df['timestamp'].dt.minute/60
avg_by_time = df.groupby('time')['occ_rate'].mean()
avg_by_time.plot(figsize=(6,4))
plt.title("Avg. Occupancy Rate by Time of Day")
plt.xlabel("Hour of day")
plt.ylabel("Avg occ_rate")
plt.grid(); plt.show()
# 5. Special day vs. normal day
df.boxplot(column='occ_rate', by='special', figsize=(6,4))
plt.title("Occ Rate: Special (1) vs. Normal (0)")
plt.suptitle("")
plt.ylabel("occupancy / capacity")
plt.show()
Missing rates (%):
lot_id 0.0
capacity 0.0
lat 0.0
lon 0.0
occupancy 0.0
vehicle 0.0
traffic 0.0
queue 0.0
special 0.0
timestamp 0.0
dtype: float64
Numeric summary:
capacity occupancy queue
count 18368.000000 18368.000000 18368.000000
mean 1605.214286 731.084059 4.587925
std 1131.153886 621.164982 2.580062
min 387.000000 2.000000 0.000000
25% 577.000000 322.000000 2.000000
50% 1261.000000 568.000000 4.000000
75% 2803.000000 976.000000 6.000000
max 3883.000000 3499.000000 15.000000
# 1. Queue growth rate (delta per record within each lot)
df = df.sort_values(['lot_id','timestamp'])
df['queue_prev'] = df.groupby('lot_id')['queue'].shift(1).fillna(0)
df['queue_diff'] = df['queue'] - df['queue_prev']
# 2. Time-of-day cyclical
df['tod_sin'] = np.sin(2*np.pi*df['time']/24)
df['tod_cos'] = np.cos(2*np.pi*df['time']/24)
# 3. Update our stream-source with new columns
# (Just re-create the StreamSubject and reload into Pathway)
stream = pw.io.python.read(
StreamSubject(df),
schema=Schema,
autocommit_duration_ms=50
)
# Simple grid search over demand weights [w_occ, w_queue]
results = []
for w_occ in [0.3,0.5,0.7]:
for w_q in [0.1,0.3,0.5]:
def price_demand_tuned(occupancy, capacity, queue, traffic, special, vehicle, base=BASE_PRICE):
occ_rate = occupancy/capacity
v_w = VEHICLE_WEIGHTS.get(vehicle, 0)
t_w = TRAFFIC_WEIGHTS.get(traffic, 0)
demand = w_occ*occ_rate + w_q*queue - 0.2*t_w + 0.1*special + v_w
demand = np.clip(demand,0,1)
price = base*(1+demand)
return np.clip(price, base*0.5, base*2)
# Evaluate smoothness = avg |āprice/āocc_rate|
df['p'] = df.apply(lambda r: price_demand_tuned(
r.occupancy,r.capacity,r.queue,r.traffic,r.special,r.vehicle), axis=1)
df['p_prev'] = df.groupby('lot_id')['p'].shift(1)
smoothness = np.nanmean(np.abs((df['p']-df['p_prev'])/(df['occ_rate']-df.groupby('lot_id')['occ_rate'].shift(1)+1e-6)))
results.append((w_occ, w_q, smoothness))
# Show top 3 settings
sorted(results, key=lambda x: x[2])[:3]
[(0.7, 0.5, np.float64(3871.5598800096245)), (0.5, 0.5, np.float64(4418.144139323986)), (0.3, 0.5, np.float64(4964.766508763))]
This cell re-implements the competitive pricing logic using pandas DataFrames.
- Reload Prices and Map Capacity: It reloads the prices from the CSV file and maps the capacity to each lot using the original
df. - Define
comp_priceFunction: Defines a functioncomp_pricethat takes a group of rows (for a specific timestamp) as input and calculates the competitive price (price3) for each lot within that group. It finds nearby lots, checks their prices, and adjusts the base price (price2) if the lot is full and competitor prices are significantly different. - Apply
comp_price: Applies thecomp_pricefunction to each group of rows based on the timestamp, effectively calculatingprice3for all data points. - Inspect Results: Displays the first 10 rows of the DataFrame, showing the timestamp, lot ID, demand price (
price2), and the newly calculated competitive price (price3).
# Pandas-Based Competitive Pricing ---
import pandas as pd
from geopy.distance import geodesic
# 1) Reload your prices
df_prices = pd.read_csv("/tmp/prices.csv")
df_prices['ts'] = pd.to_datetime(df_prices['ts'])
# 2) Map each lotās capacity
cap_map = df[['lot_id','capacity']].drop_duplicates().set_index('lot_id')['capacity']
df_prices['capacity'] = df_prices['lot'].map(cap_map)
# 3) Competitive logic per timestamp
def comp_price(gr):
# Build lotādemand_price map
price_map = dict(zip(gr['lot'], gr['price2']))
out = []
for _, row in gr.iterrows():
base = row['price2']
occ, cap = row['occ'], row['capacity']
loc_i = lot_locations[row['lot']]
# find nearby lot_ids
nearby = [
other for other, loc in lot_locations.items()
if other != row['lot']
and geodesic((loc_i['lat'],loc_i['lon']),
(loc['lat'],loc['lon'])).km <= 1.0
]
# collect their prices
comp_ps = [price_map[o] for o in nearby if o in price_map]
new_p = base
if occ >= cap and comp_ps:
if min(comp_ps) < base:
new_p = base * 0.95
elif max(comp_ps) > base:
new_p = base * 1.05
out.append(new_p)
return pd.Series(out, index=gr.index)
# 4) Apply and assign price3
df_prices['price3'] = (
df_prices
.groupby('ts', group_keys=False)
.apply(comp_price)
)
# 5) Inspect
df_prices[['ts','lot','price2','price3']].head(10)
/tmp/ipython-input-18-884290514.py:45: FutureWarning: DataFrameGroupBy.apply operated on the grouping columns. This behavior is deprecated, and in a future version of pandas the grouping columns will be excluded from the operation. Either pass `include_groups=False` to exclude the groupings or explicitly select the grouping columns after groupby to silence this warning. .apply(comp_price)
| ts | lot | price2 | price3 | |
|---|---|---|---|---|
| 0 | 2016-10-04 02:29:00+00:00 | BHMNCPHST01 | 17.187500 | 17.187500 |
| 1 | 2016-10-04 02:29:00+00:00 | BHMMBMMBX01 | 18.421397 | 18.421397 |
| 2 | 2016-10-04 02:29:00+00:00 | BHMBCCMKT01 | 14.028596 | 14.028596 |
| 3 | 2016-10-04 02:29:00+00:00 | BHMNCPNST01 | 19.067010 | 19.067010 |
| 4 | 2016-10-04 02:29:00+00:00 | BHMEURBRD01 | 17.744681 | 17.744681 |
| 5 | 2016-10-04 02:29:00+00:00 | Shopping | 17.598958 | 17.598958 |
| 6 | 2016-10-04 02:29:00+00:00 | Broad Street | 17.789855 | 17.789855 |
| 7 | 2016-10-04 02:29:00+00:00 | BHMBCCTHL01 | 18.050388 | 18.050388 |
| 8 | 2016-10-04 02:29:00+00:00 | Others-CCCPS8 | 20.000000 | 20.000000 |
| 9 | 2016-10-04 02:29:00+00:00 | Others-CCCPS105a | 18.264559 | 18.264559 |
# Reload df_prices if you need to
df_prices = pd.read_csv("/tmp/prices.csv", parse_dates=['ts'])
# 1a) Reābuild capacity map directly from original df
cap_map = df.groupby('lot_id')['capacity'].first().to_dict()
df_prices['capacity'] = df_prices['lot'].map(cap_map)
# 1b) How many rows are āfullā?
df_prices['is_full'] = df_prices['occ'] >= df_prices['capacity']
print("Total fullālot events:", df_prices['is_full'].sum())
# 1c) Show any mismatches or NaNs in capacity
print("Unique capacities:", sorted(df_prices['capacity'].dropna().unique()))
print("Rows with missing cap:", df_prices[df_prices['capacity'].isna()][['lot']].drop_duplicates())
Total fullālot events: 255 Unique capacities: [np.int64(387), np.int64(470), np.int64(485), np.int64(577), np.int64(687), np.int64(690), np.int64(1200), np.int64(1322), np.int64(1920), np.int64(2009), np.int64(2803), np.int64(2937), np.int64(3103), np.int64(3883)] Rows with missing cap: Empty DataFrame Columns: [lot] Index: []
import pandas as pd
from geopy.distance import geodesic
# reload
dfp = pd.read_csv("/tmp/prices.csv", parse_dates=['ts'])
# rebuild capacity map
cap_map = df[['lot_id','capacity']].drop_duplicates().set_index('lot_id')['capacity']
dfp['capacity'] = dfp['lot'].map(cap_map)
# focus on fullālot events
full = dfp[dfp['occ'] >= dfp['capacity']]
def count_comps(row):
loc_i = lot_locations.get(row['lot'])
if not loc_i:
return 0
return sum(
1
for other, loc in lot_locations.items()
if other!=row['lot']
and geodesic((loc_i['lat'],loc_i['lon']),
(loc['lat'],loc['lon'])).km<=1.0
)
full['n_comps'] = full.apply(count_comps, axis=1)
print(full['n_comps'].value_counts())
n_comps 12 245 3 4 0 4 2 2 Name: count, dtype: int64
/tmp/ipython-input-20-819062653.py:25: SettingWithCopyWarning: A value is trying to be set on a copy of a slice from a DataFrame. Try using .loc[row_indexer,col_indexer] = value instead See the caveats in the documentation: https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy full['n_comps'] = full.apply(count_comps, axis=1)
import pandas as pd
from geopy.distance import geodesic
# 1) Reload and reconstruct capacity
dfp = pd.read_csv("/tmp/prices.csv", parse_dates=['ts'])
cap_map = df.groupby('lot_id')['capacity'].first().to_dict()
dfp['capacity'] = dfp['lot'].map(cap_map)
# 2) Compute competitor counts without slice warnings
def count_comps(row):
loc_i = lot_locations[row['lot']]
return sum(
1 for other, loc in lot_locations.items()
if other != row['lot']
and geodesic((loc_i['lat'],loc_i['lon']),
(loc['lat'],loc['lon'])).km <= 1.0
)
dfp = dfp.copy() # drop any viewāofāslice issues
dfp['n_comps'] = dfp.apply(count_comps, axis=1)
print(dfp['n_comps'].value_counts())
# Should match: 12 245, 3 4, 0 4, 2 2
# 3) Reāapply comp_price
def comp_price(gr):
price_map = dict(zip(gr['lot'], gr['price2']))
out = []
for _, r in gr.iterrows():
base, occ, cap = r['price2'], r['occ'], r['capacity']
loc_i = lot_locations[r['lot']]
# nearby lots
comp_ps = [
price_map[o] for o,loc in lot_locations.items()
if o!=r['lot']
and geodesic((loc_i['lat'],loc_i['lon']),
(loc['lat'],loc['lon'])).km <= 1.0
and o in price_map
]
new = base
if occ >= cap and comp_ps:
if min(comp_ps) < base:
new = base * 0.95
elif max(comp_ps) > base:
new = base * 1.05
out.append(new)
return pd.Series(out, index=gr.index)
dfp['price3'] = (
dfp.groupby('ts', group_keys=False)
.apply(comp_price)
)
# 4) Show only the rows where price3 actually changed
diff = dfp[dfp['price3'] != dfp['price2']]
print("Total adjustments:", len(diff))
print(diff[['ts','lot','occ','capacity','n_comps','price2','price3']].head(10))
n_comps
10 7872
9 3936
12 2624
0 1312
3 1312
2 1312
Name: count, dtype: int64
Total adjustments: 37
ts lot occ capacity n_comps price2 \
11284 2016-11-19 09:34:00+00:00 BHMBCCTHL01 388 387 12 20.0
11304 2016-11-19 10:01:00+00:00 BHMBCCTHL01 388 387 12 20.0
11326 2016-11-19 10:34:00+00:00 BHMBCCTHL01 395 387 12 20.0
11337 2016-11-19 11:01:00+00:00 BHMBCCTHL01 402 387 12 20.0
11570 2016-11-20 10:31:00+00:00 BHMBCCTHL01 399 387 12 20.0
11590 2016-11-20 11:01:00+00:00 BHMBCCTHL01 387 387 12 20.0
12297 2016-11-23 09:34:00+00:00 BHMBCCTHL01 392 387 12 20.0
13062 2016-11-26 09:31:00+00:00 BHMBCCTHL01 401 387 12 20.0
13067 2016-11-26 09:58:00+00:00 BHMBCCTHL01 398 387 12 20.0
13080 2016-11-26 10:31:00+00:00 BHMBCCTHL01 402 387 12 20.0
price3
11284 19.0
11304 19.0
11326 19.0
11337 19.0
11570 19.0
11590 19.0
12297 19.0
13062 19.0
13067 19.0
13080 19.0
/tmp/ipython-input-21-2990881443.py:51: FutureWarning: DataFrameGroupBy.apply operated on the grouping columns. This behavior is deprecated, and in a future version of pandas the grouping columns will be excluded from the operation. Either pass `include_groups=False` to exclude the groupings or explicitly select the grouping columns after groupby to silence this warning. .apply(comp_price)
This cell implements the pandas-based rerouting logic, suggesting alternative parking lots for drivers when their intended lot is full.
- Ensure a clean copy: Creates a copy of the
df_pricesDataFrame to avoid modifying the original data unintentionally. - Assemble a deduplicated snapshot: Creates a DataFrame
statecontaining a unique snapshot of each lot's occupancy, capacity, and demand price (price2) at each timestamp. - Build a
state_mapdictionary: Converts thestateDataFrame into a dictionary where the keys are(timestamp, lot_id)tuples and the values are dictionaries containing the occupancy, capacity, and demand price for that lot at that time. This allows for efficient lookup of nearby lot states. - Define
suggest_reroutefunction: Defines a function that takes a row of the DataFrame as input and suggests alternative parking lots. It only provides suggestions for full lots. It finds nearby lots within 1 km and checks their state using thestate_map. If a nearby lot is not full, it's considered a candidate. The candidates are then sorted by their demand price in ascending order. - Apply to every row: Applies the
suggest_reroutefunction to each row of thedf_pricesDataFrame to generate a list of suggested reroute options (reroute_to) for each data point. - Quick check: Displays the first 10 rows of the DataFrame, showing the timestamp, lot ID, occupancy, capacity, and the list of suggested reroute options.
# PandasāBased Rerouting Logic
from geopy.distance import geodesic
# 1) Ensure a clean copy
df_prices = df_prices.copy()
# 2) Assemble a deduplicated snapshot of each lotĆtimestamp
state = (
df_prices
[['ts','lot','occ','capacity','price2']]
.drop_duplicates(subset=['ts','lot'])
)
# 3) Build a (ts, lot) ā state dict by iterating rows
state_map = {
(row.ts, row.lot): {
'occ': row.occ,
'capacity': row.capacity,
'price2': row.price2
}
for row in state.itertuples()
}
# 4) Reroute function
def suggest_reroute(row):
# only for full lots
if row.occ < row.capacity:
return []
loc_i = lot_locations[row.lot]
candidates = []
for other, loc in lot_locations.items():
if other == row.lot:
continue
# only within 1 km
if geodesic((loc_i['lat'],loc_i['lon']),
(loc['lat'],loc['lon'])).km > 1.0:
continue
key = (row.ts, other)
if key not in state_map:
continue
st = state_map[key]
# only nonāfull neighbors
if st['occ'] < st['capacity']:
candidates.append((other, st['price2']))
# sort by price ascending, return up to 3
candidates.sort(key=lambda x: x[1])
return [lot for lot,_ in candidates[:3]]
# 5) Apply to every row
df_prices['reroute_to'] = df_prices.apply(suggest_reroute, axis=1)
# 6) Quick check
df_prices[['ts','lot','occ','capacity','reroute_to']].head(10)
| ts | lot | occ | capacity | reroute_to | |
|---|---|---|---|---|---|
| 0 | 2016-10-04 02:29:00+00:00 | BHMNCPHST01 | 237 | 1200 | [] |
| 1 | 2016-10-04 02:29:00+00:00 | BHMMBMMBX01 | 264 | 687 | [] |
| 2 | 2016-10-04 02:29:00+00:00 | BHMBCCMKT01 | 61 | 577 | [] |
| 3 | 2016-10-04 02:29:00+00:00 | BHMNCPNST01 | 249 | 485 | [] |
| 4 | 2016-10-04 02:29:00+00:00 | BHMEURBRD01 | 117 | 470 | [] |
| 5 | 2016-10-04 02:29:00+00:00 | Shopping | 614 | 1920 | [] |
| 6 | 2016-10-04 02:29:00+00:00 | Broad Street | 178 | 690 | [] |
| 7 | 2016-10-04 02:29:00+00:00 | BHMBCCTHL01 | 120 | 387 | [] |
| 8 | 2016-10-04 02:29:00+00:00 | Others-CCCPS8 | 445 | 1322 | [] |
| 9 | 2016-10-04 02:29:00+00:00 | Others-CCCPS105a | 709 | 2009 | [] |
# Showing a few actual reroute suggestions
df_prices[df_prices['reroute_to'].str.len() > 0] \
[['ts','lot','occ','capacity','reroute_to']].head(5)
| ts | lot | occ | capacity | reroute_to | |
|---|---|---|---|---|---|
| 4975 | 2016-10-25 08:59:00+00:00 | BHMEURBRD01 | 470 | 470 | [BHMBCCMKT01, BHMBCCTHL01, Shopping] |
| 10457 | 2016-11-16 06:30:00+00:00 | BHMEURBRD01 | 470 | 470 | [BHMBCCMKT01, BHMBCCTHL01, Shopping] |
| 10688 | 2016-11-17 06:01:00+00:00 | BHMBCCTHL01 | 387 | 387 | [BHMBCCMKT01, BHMEURBRD01, BHMNCPHST01] |
| 10697 | 2016-11-17 06:34:00+00:00 | BHMBCCTHL01 | 390 | 387 | [BHMBCCMKT01, BHMEURBRD01, BHMNCPHST01] |
| 10713 | 2016-11-17 07:01:00+00:00 | BHMBCCTHL01 | 392 | 387 | [BHMBCCMKT01, BHMEURBRD01, BHMNCPHST01] |
This cell generates a Bokeh plot to visualize and highlight the instances where the Competitive Pricing Model (price3) differs from the Demand-Based Model (price2).
- Full source and initial filtering: Prepares the data for Bokeh by creating
ColumnDataSourceobjects for the full dataset and an initial subset for the first parking lot, including a separate source for the data points where the prices diverge. - Build figure with lines and scatter points: Creates a Bokeh figure and adds lines for
price2andprice3. Scatter points are added specifically for the data points whereprice3is different fromprice2to visually highlight these adjustments. - Add HoverTool: Includes a
HoverToolto display detailed information (timestamp, occupancy/capacity, price2, price3) when hovering over data points. - Dropdown selector and callback: Creates a dropdown to select a parking lot and implements a JavaScript callback to update the plot's data sources based on the selected lot, ensuring the plot displays the relevant data and highlights for the chosen lot.
- Render: Displays the dropdown selector and the interactive plot.
# AllāLots Dropdown + Highlighted Divergences
from bokeh.io import output_notebook, show
from bokeh.plotting import figure
from bokeh.models import ColumnDataSource, HoverTool, Select, CustomJS
from bokeh.layouts import column
output_notebook()
# 1) Full source
full_src = ColumnDataSource(df_prices)
# 2) All lots in dropdown
lots = sorted(df_prices['lot'].unique())
initial = lots[0]
# 3) Initial filtered sources
df0 = df_prices[df_prices['lot'] == initial]
src = ColumnDataSource(df0)
diff0 = df0[df0['price2'] != df0['price3']]
src_diff = ColumnDataSource(diff0)
# 4) Build figure
p = figure(x_axis_type="datetime", height=350, width=800,
title=f"Lot {initial}: Demand vs Competitive")
p.line('ts', 'price2', source=src, color="green",
legend_label="Price2 (Demand)", line_width=2)
p.line('ts', 'price3', source=src, color="red",
legend_label="Price3 (Competitive)", line_width=2)
p.scatter('ts', 'price3', source=src_diff, color="blue",
size=8, legend_label="Adjustment Points")
hover = HoverTool(tooltips=[
("Time", "@ts{%F %T}"),
("Occ/Cap","@occ/@capacity"),
("P2", "@price2{$0.00}"),
("P3", "@price3{$0.00}"),
], formatters={'@ts':'datetime'}, mode='vline')
p.add_tools(hover)
p.legend.location = "top_left"
# 5) Dropdown callback
selector = Select(title="Select Lot", value=initial, options=lots)
callback = CustomJS(args=dict(full=full_src, src=src, srcd=src_diff, sel=selector, fig=p), code="""
const d = full.data, L = sel.value;
const n = d['lot'].length;
// empty arrays
const f = {ts:[], price2:[], price3:[], occ:[], capacity:[]};
const df = {ts:[], price3:[]};
for (let i = 0; i < n; i++){
if (d.lot[i] === L){
f.ts.push(d.ts[i]); f.price2.push(d.price2[i]);
f.price3.push(d.price3[i]);f.occ.push(d.occ[i]);
f.capacity.push(d.capacity[i]);
if (d.price2[i] !== d.price3[i]){
df.ts.push(d.ts[i]); df.price3.push(d.price3[i]);
}
}
}
src.data = f; src.change.emit();
srcd.data = df; srcd.change.emit();
fig.title.text = `Lot ${L}: Demand vs Competitive`;
""")
selector.js_on_change('value', callback)
# 6) Render
show(column(selector, p))
This cell downloads the generated /tmp/prices.csv file, which contains the calculated prices from the three pricing models and other relevant data.
from google.colab import files
files.download('/tmp/prices.csv')